namespace NHCodeFirst
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Linq.Expressions;
    using NHibernate.Linq;

    public class DbSetBase<T, TChild> : IQueryable<T> where T : class
    {
        protected readonly DbContext dbContext;
        private readonly ObservableCollection<T> local;
        protected readonly IQueryable<T> queryable;
        protected TChild child;

        public DbSetBase(DbContext context)
        {
            dbContext = context;
            queryable = dbContext.Session.Query<T>();
            local = new ObservableCollection<T>();
            local.CollectionChanged += OnLocalOnCollectionChanged;
        }

        public ObservableCollection<T> Local
        {
            get
            {
                var loadedItems = dbContext.Session.GetSessionImplementation().PersistenceContext.EntityEntries.OfType<DictionaryEntry>()
                    .Select(entry => entry.Key).OfType<T>()
                    .Where(item => !local.Contains(item));
                local.CollectionChanged -= OnLocalOnCollectionChanged;
                foreach (var loadedItem in loadedItems)
                    local.Add(loadedItem);
                local.CollectionChanged += OnLocalOnCollectionChanged;
                return local;
            }
        }

        public IEnumerator<T> GetEnumerator()
        {
            return queryable.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return queryable.GetEnumerator();
        }

        public Expression Expression
        {
            get { return queryable.Expression; }
        }

        public Type ElementType
        {
            get { return queryable.ElementType; }
        }

        public IQueryProvider Provider
        {
            get { return queryable.Provider; }
        }

        private void OnLocalOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            if (args.Action == NotifyCollectionChangedAction.Add)
                Add(args.NewItems.OfType<T>());
            if (args.Action == NotifyCollectionChangedAction.Remove)
                Remove(args.OldItems.OfType<T>());
        }

        public void Add(T entity)
        {
            if (!dbContext.AddedItems.Contains(entity))
                dbContext.AddedItems.Add(entity);
            local.CollectionChanged -= OnLocalOnCollectionChanged;
            if (!local.Contains(entity))
                local.Add(entity);
            local.CollectionChanged += OnLocalOnCollectionChanged;
        }

        public TChild Load()
        {
            this.ToList();
            return child;
        }

        public TChild Load(Expression<Func<T,bool>> predicate)
        {
            this.Where(predicate).ToList();
            return child;
        }

        public void Add(IEnumerable<T> entities)
        {
            foreach (var entity in entities)
                Add(entity);
        }

        public void Remove(T entity)
        {
            if (dbContext.Session.GetSessionImplementation().PersistenceContext.GetEntry(entity) != null)
                dbContext.DeletedItems.Add(entity);
            if (dbContext.AddedItems.Contains(entity))
                dbContext.AddedItems.Remove(entity);
            local.CollectionChanged -= OnLocalOnCollectionChanged;
            if (local.Contains(entity))
                local.Remove(entity);
            local.CollectionChanged += OnLocalOnCollectionChanged;
        }

        public void Remove(params object[] id)
        {
            foreach (var identifier in id)
                Remove(Find(identifier));
        }

        public void Remove(IEnumerable<T> entities)
        {
            foreach (var entity in entities)
                Remove(entity);
        }

        public T Find(object id)
        {
            return dbContext.Find<T>(id);
        }

        public IEnumerable<T> Find(params object[] ids)
        {
            var items = new List<T>();
            ids.ForEach(id => items.Add(dbContext.Find<T>(id)));
            return items;
        }
    }
}